/*
 * ModelCC, distributed under ModelCC Shared Software License, www.modelcc.org
 */

package org.modelcc.io.java;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import org.modelcc.IModel;
import org.modelcc.Pattern;
import org.modelcc.Priority;
import org.modelcc.io.ModelReader;
import org.modelcc.language.metamodel.MemberCollectionType;
import org.modelcc.language.metamodel.LanguageModel;
import org.modelcc.metamodel.Model;
import org.modelcc.metamodel.ModelElement;

/**
 * Abstract Java model reader. Template algorithm outline:
 * 
 * <pre>
 * createMetadata()
 * 
 * for each Java class:
 *   createClassElement()
 *   preProcessClass()
 *   for each field:
 *     createFieldElement()
 *     processField()
 *   postProcessClass()
 * 
 * createModel()
 * </pre>
 * 
 * @author Fernando Berzal (fberzal@modelcc.org) & Luis Quesada (lquesada@modelcc.org)
 *
 * @param <M> ModelCC model
 * @param <C> Class model element
 * @param <F> Field model element
 * @param <D> Model reader metadata
 */
public abstract class JavaModelReader<M extends Model,
                                      C extends ModelElement,
                                      F extends ModelElement,
                                      D extends JavaModelReaderMetadata<C>> extends ModelReader<M> 
{
	/**
	 * Root class.
	 */
	protected Class root;


    /**
     * Constructor
     * @param root the root class
     */
    public JavaModelReader(Class root) 
    {
        this.root = root;
    }
	

	/**
	 * Read a model from a root java class.
	 * @return the model
	 * @throws Exception
	 */
	@Override
	public final M read() throws Exception 
	{
		clearMessages();
	
		if (!IModel.class.isAssignableFrom(root))
			throw new ClassDoesNotExtendIModelException("Class "+root+" does not extend IModel.");
	
		D metadata = createMetadata();
	
		metadata.setRelevantClasses(detectRelevantClasses(root));
		
		readClasses(metadata);
		
		return createModel(metadata);
	}
	
	/**
	 * Reads a model from a given class (included for ensuring backwards compatibility)
     *
     * @param type the root class of the model
     * @return the ModelCC languagemodel
     * @throws Exception
     */
	public static final LanguageModel read (Class type)
		throws Exception
	{
        JavaLanguageReader jmr = new JavaLanguageReader(type);

        return jmr.read();
	}
	
    /**
     * Create specific instance of JavaModelReaderMetadata
     * @return Reader metadata
     */
    public abstract D createMetadata ();
	
    /**
     * Create ModelCC model from JavaModelReader metadata
     * @param metadata Metadata obtained through reflection
     * @return The resulting ModelCC model
     * @throws Exception
     */
	public abstract M createModel (D metadata) 
			throws Exception;
    

	protected abstract void preProcessClass (Class type, D metadata, C element)
			throws Exception; 


    protected abstract void postProcessClass (Class type, D metadata, C element)
			throws Exception; 
    
    /**
     * Create ModelCC class model from Java metadata
     */
	protected abstract C createClassElement (Class type);
	
	/**
	 * Create ModelCC field model from Java metadata
	 */
	protected abstract F createFieldElement (Field field, Class type, D metadata); 

	/**
	 * Field processing
	 */
	protected abstract void processField (C element, F field);
	

    /**
     * Reads all the classes information
     * @param metadata reader metadata
     * @throws Exception
     */
    private void readClasses(D metadata)
    		throws Exception
    {
    	C el;

    	for (Class c: metadata.getRelevantClasses()) {
    		el = readClass(c,metadata);
    		metadata.getJavaElements().add(el);
    	}

    	for (Class cact: metadata.getRelevantClasses()) {
    		if (!cact.isPrimitive()) {
    			Class sclass = cact.getSuperclass();
    			if (IModel.class.isAssignableFrom(sclass)) {
    				metadata.addJavaSubclass(metadata.getClassElement(sclass), metadata.getClassElement(cact));
    			}
    		}
    	}
    }
    
    /**
     * Read class information
     * @param type the element class
     * @param metadata reader metadata
     * @throws Excception
     */
    private C readClass(Class type, D metadata)
    	throws Exception
    {
    	C element = createClassElement(type);
		    	    	    	
    	preProcessClass(type, metadata, (C) element);

    	// Field members
    	
    	Field[] fields = type.getDeclaredFields();

    	for (Field field: fields) {
    		F c = createFieldElement(field,type,metadata);
    		if (c != null) {
    			if (!Modifier.isStatic(field.getModifiers())) {
    				element.addMember(c);
    				processField((C)element, c);
    			}
    		}
    	}

    	postProcessClass(type, metadata,(C) element);
    	
    	metadata.setClassElement(type,element);

    	return element;
    }
    
    
    /**
     * Detects all the relevant classes from a root one
     * @param root the root class
     * @return the set of relevant classes
     * @throws ClassNotFoundException
     */
    private Set<Class> detectRelevantClasses(Class root) throws ClassNotFoundException 
    {
    	LinkedList<Class> q = new LinkedList<Class>();
    	Set<Class> done = new HashSet<Class>();

    	Class currentClass;

    	// Hack to avoid processing String and Object classes, which makes no sense.
    	done.add(String.class);
    	done.add(Object.class);

    	if (!root.getName().contains("$"))
    		q.addLast(root);

    	while (!q.isEmpty()) {
    		currentClass = q.removeLast();
    		done.add(currentClass);

    		// Add precedences
    		if (currentClass.isAnnotationPresent(Priority.class)) {
    			Priority an;
    			an = (Priority) currentClass.getAnnotation(Priority.class);
    			for (int j = 0;j < an.precedes().length;j++) {
    				if (IModel.class.isAssignableFrom(an.precedes()[j]))
    					addClass(an.precedes()[j], q, done);
    			}
    		}

    		// Add enclosing class 
    		if (currentClass.getEnclosingClass() != null)
    			if (IModel.class.isAssignableFrom(currentClass.getEnclosingClass()))
    				addClass(currentClass.getEnclosingClass(),q,done);

    		// Field processing (Containers, vectors and bare fields).
    		if (!Reflection.hasAnnotation(currentClass,Pattern.class)) {
    			Field[] fl = currentClass.getDeclaredFields();
    			for (int i = 0;i < fl.length;i++) {
    				MemberCollectionType collection = null;
    				boolean avoid = false;
    				if (List.class.isAssignableFrom(fl[i].getType())) {
    					if (ArrayList.class.equals(fl[i].getType()) || (List.class.equals(fl[i].getType())))
    						collection = MemberCollectionType.LIST;
    					else
    						log(Level.SEVERE, "In field \"{0}\" of class \"{1}\": The class of a composite list may only be List or ArrayList.", new Object[]{fl[i].getName(), currentClass.getCanonicalName()});
    				} else if (Set.class.isAssignableFrom(fl[i].getType())) {
    					if (HashSet.class.equals(fl[i].getType()) || (Set.class.equals(fl[i].getType())))
    						collection = MemberCollectionType.SET;
    					else
    						log(Level.SEVERE, "In field \"{0}\" of class \"{1}\": The class of a composite set may only be Set or HashSet.", new Object[]{fl[i].getName(), currentClass.getCanonicalName()});
    				} else if (Map.class.isAssignableFrom(fl[i].getType())) {
    					avoid = true;
    				} else if (fl[i].getType().isArray()) {
    					collection = MemberCollectionType.ARRAY;
    				}

    				if (!avoid) {
    					Class add = Reflection.getType(collection,fl[i]);
    					if (add != null) {
    						if (IModel.class.isAssignableFrom(currentClass))
    							addClass(add, q, done);
    					}
    				}
    			}
    		}

    		// Add subclasses
    		if (!currentClass.isPrimitive()) {
    	    	Set<Class> subclasses = Reflection.findSubclasses(currentClass);
    			for (Class add: subclasses) {
    				addClass(add, q, done);
    			}
    		}

    		// Add superclass
    		if (!currentClass.isPrimitive()) {
    			addClass( currentClass.getSuperclass(), q, done);
    		}

    	}

    	// Revert hack for avoiding the processing of String and Object classes
    	done.remove(Object.class);
    	done.remove(String.class);

    	return done;
    }

    
    /**
     * Adds a class to a queue if it hasn't been added before
     * @param add the class to add
     * @param q the queue
     * @param done the already added classes
     * @return true if it has been added now, false if it was added before
     * @throws ClassNotFoundException
     */
    private boolean addClass(Class add,LinkedList<Class> q,Set<Class> done) 
    		throws ClassNotFoundException 
    {
    	if (!done.contains(add) && !q.contains(add) && IModel.class.isAssignableFrom(add) && !add.getName().contains("$")) {
    		q.addLast(add);
    		return true;
    	}
    	return false;
    }    
}